iT邦幫忙

2025 iThome 鐵人賽

DAY 20
3
Modern Web

從 Canvas 到各式各樣的 Web API 之旅系列 第 20

Day 20 - Web Animations API 原理:為什麼把動畫交給 compositor 更滑順?撲克牌堆疊、收合、洗牌示範

  • 分享至 

  • xImage
  •  

過去幾天從多媒體、分享、裝置能力一路走到使用者體驗。今天換個節奏,來一個 畫面動起來 的主題:Web Animations API(WAAPI)。這個 API 讓我們用 JavaScript 直接驅動瀏覽器的動畫引擎,拿到 時間軸撥放控制效能優勢,並能和 CSS 動畫互補。💓

如果你有做過動畫,可能習慣兩種方式:

  • CSS Transition / Animation:寫在 CSS 裡,動畫會自動跑完,很適合簡單的進場、hover 效果。
  • JavaScript 手動改 style:用 setTimeoutrequestAnimationFrame 不斷改 style.leftstyle.top,雖然能控制,但效能差、程式碼也麻煩。

WAAPI 結合了兩者的好處

  • 你可以在 JS 裡用 element.animate(...) 建立動畫,得到一個 Animation 物件
  • 這個物件就像一個「時間軸遙控器」:能播放、暫停、加速、倒帶,甚至等動畫結束。
  • 更重要的是,實際的畫面更新並不是 JS 自己在算,而是交給瀏覽器內部的「動畫引擎(compositor)」,效能跟 CSS 動畫一樣流暢。

換句話說:WAAPI 讓你既能寫程式控制動畫,又能享受瀏覽器原生的高效能。

前面會花比較多篇幅講原理,想直接看到應用,可以跳到 "基本用法"、"範例 Demo" 🏃‍♂️


一、先來介紹「交給瀏覽器的動畫引擎(compositor)」是什麼、為什麼重要

什麼是「動畫引擎 / compositor」

瀏覽器要把一個網頁畫到螢幕上,其實經過好幾個階段:

  1. JavaScript:執行你的程式邏輯,例如 DOM 操作、事件監聽。
  2. Style:計算每個元素應該套用哪些 CSS 樣式。
  3. Layout (Reflow):決定元素在頁面上的位置與大小。
  4. Paint:把元素的外觀(顏色、邊框、字型)畫成位圖。
  5. Compositing:最後一步,把不同的圖層(layer)疊合起來,套用位移、縮放、旋轉、透明度等效果(例如 transform / opacity),交給 GPU 合成畫面。

所謂的 compositor,就是第 5 步的「合成器」。瀏覽器的動畫引擎其實包含了許多部分,其中最關鍵、影響效能的就是 compositor。它不會重新計算排版,也不會重畫像素,只負責把已經畫好的圖層 搬動、縮放、旋轉、改透明度,並用 GPU 把畫面「組合」起來,效率非常高。當我們說「交給瀏覽器的動畫引擎(compositor)」時,指的就是這個過程。

為什麼「交給 compositor」這件事很重要

  1. 不卡 JavaScript 主執行緒
    • JavaScript、Style、Layout、Paint 都跑在「主執行緒」。一旦 JS 寫了很重的程式或 Layout 等計算量大,畫面就可能卡住。
    • 但 compositor 可以在 Compositor thread 合成執行緒 上獨立運作,即使 JS 忙著跑別的邏輯,動畫還是能持續流暢更新。
  2. 高幀率與低延遲
    • 螢幕每 16.7ms(60FPS)或 8.3ms(120FPS)就要更新一幀。如果你動畫的是會觸發 Layout/Paint 的屬性(例如 left/width/background-color),動畫必須回到主執行緒計算與重畫,一忙就掉幀
    • Compositor 只需要更新矩陣運算(例如 translateX = 50px → 100px),計算量小很多,因此幀率更穩。
  3. 省電與效能更佳
    • 跳過 Style、Layout 與 Paint,CPU 壓力減少,改由 GPU 處理。對行動裝置來說,這代表更好的續航力。
  4. 互動更即時
    • 很多觸控或捲動效果(像是慣性滑動)都是由 compositor 處理。如果動畫也放在 compositor 層,手勢和動畫可以更緊密結合,使用者體驗會更順手。

注意事項

  • 動畫時盡量只改 transformopacity,這些屬性能直接交給 compositor 處理。
  • 避免動 lefttopwidthheight 這些會觸發 Layout/Paint 的屬性,不然動畫還是得回到主執行緒,容易掉幀。
  • 如果想確保元素會被單獨提升成一個 layer,可以暫時使用 will-change(但別長期濫用,否則浪費記憶體)。

小結:compositor = 瀏覽器的動畫加速器。WAAPI 的強大之處,不只是你能用程式控制時間軸,更在於它能讓動畫直接在 compositor 層執行,做到「控制力 + 高效能」兼得。


二、延伸閱讀:和 Day 5 的關聯

還記得嗎~ 在「Day 5 - 已經有 DOM + CSS,為何需要 Canvas!?」我們比較了 DOM + CSSCanvas

  • Canvas 雖然效能比大量 DOM 穩定,但它仍然需要 主執行緒用 JS 把像素畫進 buffer,只是因為只管一個 <canvas>,省掉了 DOM 管理成本。

本篇講的 WAAPI,如果動畫的是 transform / opacity 等屬性:

  • 主執行緒幾乎不用動手畫,每幀的補間與渲染交給 compositor 全程處理
  • 這意味著主執行緒被完全解放,即使 JS 還在忙,動畫也能保持流暢。

👉 兩者雖然都能做動畫,但在流程上的差別是:

  • Canvas:主執行緒還是要畫(但只畫一次 buffer)。
  • WAAPI:主執行緒不畫,由 compositor 全程跑補間。

因此效能上 WAAPI 的動畫路徑更優,但 Canvas 在需要逐像素控制、大量繪製時仍然不可替代。


三、WAAPI 的核心原理

前面我們講過,compositor 才是動畫效能的關鍵。那麼 WAAPI 的角色是什麼?簡單來說:

WAAPI = 用 JavaScript 建立並操控一條動畫時間軸,而實際的補間運算與繪製交由瀏覽器的合成器(compositor)在 GPU 層執行。

重點:

  • 同一套動畫引擎:CSS Transition/Animation 與 WAAPI 最終都交給瀏覽器的動畫/合成器執行;差別在作者介面(CSS 宣告式 vs. JS 命令式)。
  • 時間軸(Timeline)+ 效果(KeyframeEffect):WAAPI 以 時間軸 推進 關鍵幀效果,生成中間幀(補間:瀏覽器根據 Timeline + Keyframes 算出中間狀態值)。你得到一個 Animation 物件=可被暫停、反轉、加速、seek 的時間軸控制器。
  • 合成層優先:若只動 transform/opacity,多數情況直接在 compositor 執行,避開 layout/reflow,幀率穩定。
  • 樣式不回寫fill:'forwards' 只是把最終畫面保留在合成結果,並沒有真的改 DOM 樣式,要自己手動設值才能永久保留。

四、那為什麼不是只用 CSS?

CSS Transition/Animation

  • ✅ 宣告式、樣式就地、語意清楚。
  • ✅ 由瀏覽器最佳化,通常很省資源。
  • ❌ 難以在互動流程中做精細控制(暫停、反轉、調速、手動 seek)。

WAAPI

  • ✅ 取得 Animation 物件 → 可程式化控制(時間軸、狀態、事件)。
  • ✅ 與 compositor 整合良好,動起來流暢。
  • ✅ 適合複合互動(拖曳、滾動耦合、播放列控制)。
  • ❌ 需要寫 JS,樣式與動作分散,需注意維護性。

小結:樣式驅動的單純轉場 → CSS;需要時間軸控制/互動耦合 → WAAPI。


五、基本用法

element.animate(keyframes, options)

<button id="btn">Animate me</button>
<script>
  const btn = document.getElementById('btn');
  btn.addEventListener('click', () => {
    const animation = btn.animate(
      [
        { transform: 'translateY(0)',   opacity: 1 },
        { transform: 'translateY(-8px)', opacity: .8, offset: 0.5 },
        { transform: 'translateY(0)',   opacity: 1 }
      ],
      {
        duration: 400,
        easing: 'cubic-bezier(.2,.8,.2,1)',
        iterations: 1,
        fill: 'none'
      }
    );
  });
</script>

參數

  • keyframes:陣列或 KeyframeEffect,每一格定義可動畫的 CSS 屬性(例如 transform、opacity、filter…)。
    • offset(0~1):代表在整個時間軸上的相對進度。未指定時,瀏覽器會「平均分佈」各幀(例如 3 幀 → 0、0.5、1)。
    • 屬性值會在相鄰的兩個 keyframe 之間做「補間運算」。
  • options:決定時間軸與播放行為。
    • duration:動畫時長(毫秒)。
    • easing:整段動畫的預設補間(若 keyframe 自帶 easing,會覆蓋那一段)。ease, linear, ease-in-out, cubic-bezier(...)
    • iterations:重複次數(數字或 Infinity)。
    • iterationStart:從第幾次循環的「部分」開始(例如 0.5 代表從第 1 次的一半開始)。
    • direction:翻轉播放方向或交錯。normal | reverse | alternate | alternate-reverse
    • fill:是否在動畫前後保留合成結果。none | forwards | backwards | both
    • delay / endDelay:開始前/結束後的延遲(毫秒)。

提醒fill: 'forwards' 只保留在渲染層,只是把最終畫面保留在合成結果,並沒有真的改 DOM 樣式,若要「真正」定著,請在 finished 後手動設值。

時間軸遙控器:Animation 物件

const anim = el.animate([...], { duration: 600, fill: 'both' });
anim.pause();       // 暫停
anim.play();        // 播放
anim.reverse();     // 反向播放
anim.cancel();      // 取消並還原
anim.finish();      // 直接跳到最後狀態
anim.playbackRate = 2; // 加速兩倍
await anim.finished; // 以 Promise 等動畫結束

事件與狀態

const anim = el.animate([...], { duration: 500 });
anim.onfinish = () => console.log('done');
anim.oncancel = () => console.log('canceled');

anim.finished
  .then(() => console.log('finished: resolved'))
  .catch(err => console.log('finished: rejected', err.name));

console.log(anim.playState); // 'idle' | 'running' | 'paused' | 'finished'

六、與 CSS 的分工與效能心法

  • 可用 CSS 設初始與長期樣式;用 WAAPI 控制轉場與時間軸。
  • 動畫結束後需「定著」的樣式,記得在 finished 寫回 style 或加上 class,讓 CSS 接手維持穩態。
  • 效能守則:把動畫交給瀏覽器合成器(compositor)。盡量用 transform / opacity,避免 width/height/left/top 等會觸發 layout/reflow 的屬性,幀率更穩、耗電更低。
const anim = card.animate(
  [ { transform:'translateX(-8px)', opacity:.8 }, { transform:'none', opacity:1 } ],
  { duration: 260, fill:'forwards' }
);
anim.finished.then(() => {
  card.style.transform = 'none';
  card.style.opacity = '1';
});

七、進階控制

1) 多支動畫同步

const a1 = box.animate([{ transform:'translateX(0)' }, { transform:'translateX(60px)' }], { duration: 300, fill:'forwards' });
const a2 = box.animate([{ opacity: .5 }, { opacity: 1 }], { duration: 300, fill:'forwards' });

Promise.all([a1.finished, a2.finished]).then(() => console.log('both done'));

2) KeyframeEffect 與 Timeline

  • 不透過 element.animate,而是先用 KeyframeEffect(target, keyframes, options) 將「作用元素+關鍵幀+選項」封裝成可重用效果
  • new Animation(effect, document.timeline) 它掛到一條時間軸上建立動畫,play() 播放。
  • 優點:同一效果可重播/共用、可換自訂時間軸(如實驗中的 ScrollTimeline)、更容易做序列與同步。
const effect = new KeyframeEffect(
  el,
  [ { opacity:0 }, { opacity:1 } ],
  { duration: 300, fill:'forwards' }
);
const anim = new Animation(effect, document.timeline);
anim.play();

八、第三方動畫庫

  • GSAP:功能最強大,跨瀏覽器、複雜序列控制;若專案已使用,WAAPI 可作輕量補充。
  • Framer Motion(React):宣告式,專為 React 設計的動畫函式庫,內部可能結合 CSS/WAAPI。近年在 React 生態圈非常流行,可以說是 React 主流動畫解決方案,沒用過的一定要試試~ 🎉

九、範例 Demo

上面我們已經用程式碼展示了 WAAPI 的核心用法(時間軸、控制器、事件)。
如果想直接體驗完整的互動效果,可以參考這個範例:撲克牌堆疊 → 展開 / 收合 / 洗牌動畫

撲克牌堆疊


十、小結

  • CSS:負責「靜態/穩定」的轉場樣式。
  • WAAPI:負責「互動/控制」的動態時間軸。
  • 黃金法則 → 盡量動 transform/opacity,效能最佳。
  • 善用 Animation 的控制能力,讓 UI 在互動中更順、人更有感。

👉 歡迎追蹤這個系列,我會從 Canvas 開始,一步步帶你認識更多 Web API 🎯


上一篇
Day 19 - 滑鼠鎖定、鍵盤鎖定:用 Pointer Lock API、Keyboard Lock API 增強互動
下一篇
Day 21 - 量化網頁速度!用 Performance API 看載入、繪製、資源的效能表現
系列文
從 Canvas 到各式各樣的 Web API 之旅23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言